昨天我們提到,C#中的Task是以背景執行的任務排程器透過一定的機制去輪詢(Poll)執行中的Task狀態,進一步的介紹可以看一下TaskScheduler的文件說明。
以昨天的while為例,這種瘋狂詢問狀態的做法,不可避免的會產生無效的詢問(詢問的時候Task尚未完成),要讓非同步程式高效的作法一個要點,就是盡可能讓調度程式在剛剛好的時機點詢問狀態,接下來就要介紹Rust是如何作到的:
Async-Book中採取了一個化簡過後的SimpleFuture:
trait SimpleFuture {
    type Output;
    fn poll(&mut self, wake: fn()) -> Poll<Self::Output>;
}
enum Poll<T> {
    Ready(T),
    Pending,
}
另外我們來看一個簡短的程式碼:
fn main(){
    example_runtime::block_on(async_method);
}
async fn async_method() -> impl SimpleFuture<()> {
    todo!();
}
上面程式碼中,example_runtime會提供一個非同步執行器executor來處理async_method回傳的SimpleFuture物件,executor會呼叫SimpleFuture提供的poll方法,這個行為就相當於C#中的TaskScheduler詢問Task的執行狀態,通常情況下這個SimpleFuture可以大概實作成下面這樣:
pub struct TestFuture
{
    status: bool
}
impl SimpleFuture for TestFuture {
    type Output = ();
    fn poll(&mut self, wake: fn()) -> Poll<Self::Output> {
        if self.status {
            Poll::Ready(())
        } else {
            self.status = push(wake);
            Poll::Pending
        }
    }
}
在這個實作中,執行器調用poll時,如果future已經完成了就會回傳Ready,如果沒有的話就會使用由執行器傳入的wake方法來推進整個任務的進行。
wake是什麼東西呢?讓我們從C#的例子來看,當排程器在處裡一個Task的時候,除非詢問他的狀態,否則無法知道任務是否完成,而Task本身需要被動的等待詢問。而Future trait的設計上則會在執行器輪詢的時候拿到執行器提供的方法,也就是說當任務有進展的時候,可以透過這個開口通知執行器,主動接受執行器的輪詢。
當然,非同步程式並沒有那麼簡單,rust作為一個標榜安全性的語言,實際上的future trait是長這個樣子:
pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
前面也有提到Pin是用於固定記憶體位置,而Context就是前面fn wake的強化版本,可以用來管理複雜的連線狀況。
這個設計其實很巧妙的定義了future與executor的關係,讓兩者的溝通由外層直接相依於IsCompleted變成依賴於Context這個抽象的通道,並且透過CallBack的方式反向通知上層邏輯,讓介面兩側的元件各只要關心自己的職責-任務的管理以及任務的進行。
採用無腦輪詢方案會需要為每個任務都準備一個執行緒來監看進度,舉例來說就像是網購的郵局包裹一樣,當包裹寄出之後,購買人需要一直去郵局的網站查詢包裹的貨態,才可以安排時間在家裡等待包裹,非常不經濟。
Rust的Future設計上則是由任務主動通知執行器任務完成,有點像去咖啡店點一杯咖啡,店員會給你一個呼叫器,等到呼叫器響了再去櫃檯拿咖啡就好了,盡可能在需要的時候才去問店員咖啡做好了沒。